package cmd

import (
	"context"
	"fmt"
	"go/ast"
	"go/doc"
	"go/parser"
	"go/token"
	"io"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"

	"github.com/cucumber/godog"
	"github.com/expr-lang/expr/builtin"
	"github.com/linuxsuren/api-testing/pkg/render"
	"github.com/spf13/cobra"
)

func createFunctionCmd() (c *cobra.Command) {
	opt := &funcPrinterOption{}
	c = &cobra.Command{
		Use:   "func",
		Short: "Print all the supported functions",
		RunE:  opt.runE,
	}
	flags := c.Flags()
	flags.StringVarP(&opt.feature, "feature", "", "", "The feature query")
	return
}

type funcPrinterOption struct {
	feature string
}

func (o *funcPrinterOption) runE(cmd *cobra.Command, args []string) (err error) {
	if len(args) > 0 {
		name := args[0]
		filterAndPrint(cmd, name)
	} else if o.feature != "" {
		ctx := context.WithValue(cmd.Context(), render.ContextBufferKey, cmd.OutOrStdout())

		suite := godog.TestSuite{
			ScenarioInitializer: initializeScenario,
			Options: &godog.Options{
				Format:              "pretty",
				ShowStepDefinitions: false,
				FeatureContents: []godog.Feature{{
					Name: "test",
					Contents: []byte(fmt.Sprintf(`Feature: title
					Scenario: generate
					  Given %s`, o.feature)),
				}},
				DefaultContext: ctx,
				Output:         io.Discard,
			},
		}

		if suite.Run() != 0 {
			err = fmt.Errorf("failed to query the feature")
		} else {
			cmd.Println()
		}
	} else {
		filterAndPrint(cmd, "")
	}
	return
}

func filterAndPrint(writer printer, name string) {
	if name == "" {
		writer.Println("All template functions:")
		for name, fn := range render.FuncMap() {
			writer.Println(name, reflect.TypeOf(fn))
		}
	} else {
		if fn, ok := render.FuncMap()[name]; ok {
			writer.Println(reflect.TypeOf(fn))
			desc := FuncDescription(fn)
			if desc != "" {
				writer.Println(desc)
			}
		} else {
			writer.Println("No such function")
		}
	}

	writer.Println("")
	writer.Println("All expr functions:")
	for _, item := range builtin.Builtins {
		if strings.Contains(item.Name, name) {
			writer.Println(item.Name, reflect.TypeOf(item.Func))
		}
	}
}

func initializeScenario(ctx *godog.ScenarioContext) {
	funcs := render.GetAdvancedFuncs()
	for _, fn := range funcs {
		if fn.GoDogExper != "" && fn.Generator != nil {
			ctx.Step(fn.GoDogExper, fn.Generator)
		}
	}
}

// Get the name and path of a func
func FuncPathAndName(f interface{}) string {
	return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
}

// Get the name of a func (with package path)
func FuncName(f interface{}) string {
	splitFuncName := strings.Split(FuncPathAndName(f), ".")
	return splitFuncName[len(splitFuncName)-1]
}

// Get description of a func
func FuncDescription(f interface{}) (desc string) {
	fileName, _ := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).FileLine(0)
	funcName := FuncName(f)
	fset := token.NewFileSet()

	// Parse src
	parsedAst, err := parser.ParseFile(fset, fileName, nil, parser.ParseComments)
	if err == nil {
		pkg := &ast.Package{
			Name:  "Any",
			Files: make(map[string]*ast.File),
		}
		pkg.Files[fileName] = parsedAst

		importPath, _ := filepath.Abs("/")
		myDoc := doc.New(pkg, importPath, doc.AllDecls)
		for _, theFunc := range myDoc.Funcs {
			if theFunc.Name == funcName {
				desc = theFunc.Doc
				break
			}
		}
	}
	return
}
